<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXUPSAPI
 *
 * Pulls shipping estimate from UPS using the UPS API.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Modules
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXModule, curl
 */

class GXUPSAPI extends GXModule {

	// ups api params
	private $credentials = array();
	private $classification = null;
	private $pickup = null;
	private $packing = null;
	private $shipper = array();
	private $shipto = array();
	private $contents = array();

	// stores the last error from UPS
	public $LastError = null;

	// the cross reference db
	private $db;

	// init GXModule and process conf
	public function __construct( $config = null ){
		parent::__construct();

		// key, userid, password
		$this->credentials['key']		= $config->exec->xpath('AccessKey',0)->nodeValue;
		$this->credentials['userid']	= $config->exec->xpath('UserId',0)->nodeValue;
		$this->credentials['password']	= $config->exec->xpath('Password',0)->nodeValue;
		// xml database of UPS values
		$this->db						= new GXDOM( $this->GXPage->Layout->FS->Root .$config->exec->xpath('ReferenceDB',0)->nodeValue );
		// shipper classification code, pickup code and default package type
		$this->classification			= $config->exec->xpath('Classification',0)->nodeValue;
		$this->pickup					= $config->exec->xpath('Pickup',0)->nodeValue;
		$this->packing					= $config->exec->xpath('Packing',0)->nodeValue;
		// shipping address
		$shipper = $config->exec->xpath('Shipper',0);
		$this->shipper['address']		= $shipper->exec->xpath('Address',0)->nodeValue;
		$this->shipper['city']			= $shipper->exec->xpath('City',0)->nodeValue;
		$this->shipper['state']			= $shipper->exec->xpath('State',0)->nodeValue;
		$this->shipper['postalcode']	= $shipper->exec->xpath('PostalCode',0)->nodeValue;
		$this->shipper['countrycode']	= $shipper->exec->xpath('CountryCode',0)->nodeValue;
		// static shipping options
		if( $config->exec->xpath('Other',0) ){
			$this->other = $config->exec->xpath('Other',0);
		}

		return true;
	}

	public function getOther(){
		return $this->other;
	}

	public function shipTo( $address = null, $city = null, $state = null, $postalcode = null, $countrycode = null, $residential = true ){
		$residential = $residential ? 1 : 0;
		return $this->shipto = array(
			'address'		=> $address,
			'city'			=> $city,
			'state'			=> $state,
			'postalcode'	=> $postalcode,
			'countrycode'	=> $countrycode,
			'residential' 	=> $residential
			);
	}

	public function weight( $weight = 0, $unit = 'LBS' ){
		if( !is_numeric($weight) ){
			trigger_error( 'Invalid value for weight [must be a number] (' .$weight .')', E_USER_ERROR );
			return false;
		}
		if( $unit != 'LBS' && $unit != 'KGS' ){
			trigger_error( 'Invalid value for unit [must be LBS or KGS] (' .$unit .')', E_USER_ERROR );
			return false;
		}
		return ( $this->contents['weight'] = array('value'=>$weight,'unit'=>$unit) );
	}

	public function dimensions( $length = 0, $width = 0, $height = 0, $unit = 'IN' ){
		if( !is_numeric($length) || !is_numeric($width) || !is_numeric($height) ){
			trigger_error( 'Invalid value(s) for dimensions [must be numeric] (' .$length .',' .$width .',' .$height .')', E_USER_ERROR );
			return false;
		}
		return ( $this->contents['dimensions'] = array('length'=>$length,'width'=>$width,'height'=>$height,'unit'=>$unit) );
	}

	/**
	 * callUPS
	 *
	 * Builds the UPS request and retrieves the result.
	 * Returns a GXDOM of the result.
	 * @param string $url the service url to call
	 * @param object $payload GXDOM object
	 * @return object GXDOM or false on failure
	 */
	public function callUPS( $url = null, $payload = null ){
		if( !$url || !$payload ){
			trigger_error( 'Missing url (' .$url .') or payload (' .$payload .')', E_USER_ERROR );
			return false;
		}
		// setup the request
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL,				$url);
		curl_setopt($ch, CURLOPT_HEADER,			false);
		curl_setopt($ch, CURLOPT_POST,				true);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER,	true);
		// very strange ... ups expects 2 separate xml documents concatenated together
		curl_setopt($ch, CURLOPT_POSTFIELDS,		'<' .'?xml version="1.0" ?' .'>' .$this->AccessRequest()->exec->xml() .'<' .'?xml version="1.0" ?' .'>' .$payload->exec->xml() );

		// execute the request
		$upsr = curl_exec($ch);
		$curlError = curl_errno($ch);

		// check for errors
		if( curl_errno($ch) ){
			trigger_error( 'curl err for ' .$url .': ' .curl_errno($ch) .'::' .curl_error($ch), E_USER_ERROR );
			curl_close($ch);
			return false;
		}

		// capture the result
		$response = new GXDOM($upsr);

		// make sure everything's ok
		$statuscode = $response->exec->xpath('//ResponseStatusCode',0);
		if( $statuscode && $statuscode->nodeValue == 0 ){
			$this->LastError = 'Request (' .$url .') failed: ' .$response->exec->xpath('//ErrorDescription',0)->nodeValue .' in ' .$response->exec->xpath('//ErrorLocationElementName',0)->nodeValue;
			trigger_error( $this->LastError, E_USER_ERROR );
			return false;
		}

		return $response;
	}

	/**
	 * AccessRequest
	 *
	 * Builds the base UPS xml document.
	 * @return object GXDOM
	 */
	public function AccessRequest(){
		$doc = new GXDOM('AccessRequest');
		$doc->AccessLicenseNumber( $this->credentials['key'] );
		$doc->UserId( $this->credentials['userid'] );
		$doc->Password( $this->credentials['password'] );
		return $doc;
	}

	/**
	 * RatingServiceSelectionRequest
	 *
	 * Builds the UPS xml request document which calls for a rating request (duh).
	 * @return object GXDOM
	 */
	public function RatingServiceSelectionRequest(){
		if( !$this->shipto || !$this->contents ){
			trigger_error( 'Missing shipto or contents', E_USER_ERROR );
			return false;
		}
		// clear the last error
		$this->LastError = null;

		// make the request doc
		$doc = new GXDOM('RatingServiceSelectionRequest');
		// build the request
		$request = $doc->Request();
		$request->RequestAction('Rate');
		// Single or Shop
		$request->RequestOption('Shop');
		// TransactionReference
		$ref = $request->TransactionReference();
		$ref->CustomerContext('Bare Bones Rate Request');
		$ref->XpciVersion('1.0001');
		// PickupType and CustomerClassification
		$doc->PickupType( array('Code'=>$this->pickup) );
		$doc->CustomerClassification( array('Code'=>$this->classification) );
		// Shipment, to which ShipTo, Shipper and ShipFrom are attached
		$shipment = $doc->Shipment();
		// Shipper
		$shipper = $shipment->Shipper();
		$shipperAddr = $shipper->Address();
		$shipperAddr->AddressLine1( $this->shipper['address'] );
		$shipperAddr->StateProvinceCode( $this->shipper['state'] );
		$shipperAddr->PostalCode( $this->shipper['postalcode'] );
		$shipperAddr->CountryCode( $this->shipper['countrycode'] );
		// ShipTo
		$to = $shipment->ShipTo();
		$toAddr = $to->Address();
		$toAddr->AddressLine1( $this->shipto['address'] );
		$toAddr->StateProvinceCode( $this->shipto['state'] );
		$toAddr->PostalCode( $this->shipto['postalcode'] );
		$toAddr->CountryCode( $this->shipto['countrycode'] );
		$toAddr->ResidentialAddressIndicator( $this->shipto['residential'] );
		// ShipFrom
		$from = $shipment->ShipFrom();
		$fromAddr = $from->Address();
		$fromAddr->Address( $this->shipper['address'] );
		$fromAddr->StateProvinceCode( $this->shipper['state'] );
		$fromAddr->PostalCode( $this->shipper['postalcode'] );
		$fromAddr->CountryCode( $this->shipper['countrycode'] );

		// no support for ShipFrom yet

		// no support for Service yet

		// package settings
		$package = $shipment->Package();
		$package->PackagingType( array('Code'=>$this->packing) );
		// dimensions
		if( isset($this->contents['dimensions']) ){
			$dim = $package->Dimensions();
			$dim->UnitOfMeasurement( array('Code'=>$this->contents['dimensions']['unit']) );
			$dim->Length( $this->contents['dimensions']['length'] );
			$dim->Width( $this->contents['dimensions']['width'] );
			$dim->Height( $this->contents['dimensions']['height'] );
		}
		// weight
		$weight = $package->PackageWeight();
		$weight->UnitOfMeasurement( array('Code'=>$this->contents['weight']['unit']) );
		// why are we rounding?
		$weight->Weight( round($this->contents['weight']['value'],0) );

		trigger_error( 'UPS API call: ' .$doc->exec->xml(), E_USER_ERROR );

		// send the request
		$response = $this->callUPS( 'https://www.ups.com/ups.app/xml/Rate', $doc );
		if( !$response ){
			trigger_error( 'Unable to call UPS', E_USER_ERROR );
			return false;
		}

		// check for errors in $response
		$methods = $response->exec->xpath('//Service');
		if( !count($methods) ){
			$this->LastError = 'No Service node(s) in UPS response';
			trigger_error( $this->LastError, E_USER_ERROR );
			return false;
		}

		// run through all $response options, adding a Name to the Service node
		foreach( $methods as $opt ){
			$code = $opt->exec->xpath('Code',0)->nodeValue;
			$opt->Name( $this->db->exec->xpath('//services/type[ @id = "' .$code .'" ]',0)->nodeValue );
		}

		// return exactly what we got from UPS
		return $response;
	}

	/**
	 * AddressValidationRequest
	 *
	 *
<?xml version="1.0"?>
<AccessRequest>
  <AccessLicenseNumber>TEST262223144CAT</AccessLicenseNumber>
  <UserId>REG111111</UserId>
  <Password>REG111111</Password>
</AccessRequest>
<?xml version="1.0"?>
<AddressValidationRequest xml:lang="en-US">
  <Request>
    <TransactionReference>
      <CustomerContext>Maryam Dennis-Customer Data</CustomerContext>
      <XpciVersion>1.0001</XpciVersion>
    </TransactionReference>
    <RequestAction>AV</RequestAction>
  </Request>
  <Address>
    <City>MIAMI</City>
    <StateProvinceCode>FL</StateProvinceCode>
  </Address>
</AddressValidationRequest>

<?xml version="1.0"?>
<AddressValidationResponse>
	<Response>
		<TransactionReference>
			<XpciVersion>1.0001</XpciVersion>
		</TransactionReference>
		<ResponseStatusCode>1</ResponseStatusCode>
		<ResponseStatusDescription>Success</ResponseStatusDescription>
	</Response>
	<AddressValidationResult>
		<Rank>1</Rank>
		<Quality>1.0</Quality>
		<Address>
			<City>TIMONIUM</City>
			<StateProvinceCode>MD</StateProvinceCode>
		</Address>
		<PostalCodeLowEnd>21093</PostalCodeLowEnd>
		<PostalCodeHighEnd>21094</PostalCodeHighEnd>
	</AddressValidationResult>
</AddressValidationResponse>
	 */
	public function AddressValidationRequest( $city = null, $state = null, $zip = null ){
		$doc = new GXDOM('AddressValidationRequest',array('Request'=>array('TransactionReference'=>array('CustomerContext'=>'Address Validation','XpciVersion'=>'1.001'),'RequestAction'=>'AV')));
		$addr = $doc->Address();
		if( $city ){
			$addr->City( $city );
		}
		if( $state ){
			$addr->StateOrProvince( $state );
		}
		if( $zip ){
			$addr->PostalCode( $zip );
		}

		// send the request
		$response = $this->callUPS( 'https://www.ups.com/ups.app/xml/AV', $doc );
		if( !$response ){
			trigger_error( 'Unable to call UPS', E_USER_ERROR );
			return false;
		}

		// return the entire response
		return $response;
	}

	/**
	 * TrackRequest
	 *
<?xml version="1.0" ?>
<AccessRequest xml:lang='en-US'>
	<AccessLicenseNumber>YOURACCESSLICENSENUMBER</AccessLicenseNumber>
	<UserId>YOURUSERID</UserId>
	<Password>YOURPASSWORD</Password>
</AccessRequest>
<?xml version="1.0" ?>
<TrackRequest>
	<Request>
		<TransactionReference>
			<CustomerContext>
				<InternalKey>guidlikesubstance</InternalKey>
			</CustomerContext>
			<XpciVersion>1.0</XpciVersion>
		</TransactionReference>
		<RequestAction>Track</RequestAction>
	</Request>
	<TrackingNumber>1Z9999999999999999</TrackingNumber>
</TrackRequest>
	 *
<?xml version="1.0" ?>
<TrackResponse>
	<Response>
		<TransactionReference>
			<CustomerContext>
			<InternalKey>guidlikesubstance</InternalKey>
				</CustomerContext>
			<XpciVersion>1.0</XpciVersion>
		</TransactionReference>
		<ResponseStatusCode>1</ResponseStatusCode>
		<ResponseStatusDescription>Success</ResponseStatusDescription>
	</Response>
	<Shipment>
		<Shipper>
			<ShipperNumber>123X67</ShipperNumber>
		</Shipper>
		<ShipTo>
			<Address>
				<City>Timonium</City>
				<StateProvinceCode>MD</StateProvinceCode>
				<CountryCode>US</CountryCode>
			</Address>
		</ShipTo>
		<Service>
			<Code>03</Code>
			<Description>Ground</Description>
		</Service>
		<Package>
			<TrackingNumber>1Z9999999999999999</TrackingNumber>
			<Activity>
				<ActivityLocation>
					<Address>
						<City>Timonium</City>
						<StateProvinceCode>MD</StateProvinceCode>
						<CountryCode>US</CountryCode>
					</Address>
					<Code>DL</Code>
					<Description>Delivered</Description>
				</ActivityLocation>
				<Status>
					<StatusType>
						<Code>XX</Code>
						<Description>Status XX</Description>
					</StatusType>
					<StatusCode>
						<Code>YY</Code>
						<Description>Code YY</Description>
					</StatusCode>
				</Status>
			</Activity>
		</Package>
	</Shipment>
</TrackResponse>
	 *
	 * Track a shipment by tracking number.
	 * @param string $num
	 * @param string $full
	 * @return object GXDOM
	 */
	public function TrackRequest( $num = null, $full = false ){
		$doc = new GXDOM('TrackRequest');
		$req = $doc->Request();
		$ref = $req->TransactionReference();
		$context = $ref->CustomerContext();
		$context->InternalKey('packagetracking');
		if( $full ){
			$req->RequestOption('activity');
		}
		$doc->TrackingNumber( $num );

		// call UPS
		$response = $this->callUPS( 'https://www.ups.com/ups.app/xml/Track', $doc );
		if( !$response ){
			trigger_error( 'Unable to call UPS', E_USER_ERROR );
			return false;
		}

		// return the whole response
		return $response;
	}

}

?>